3 namespace MediaWiki\Tests\Revision
;
7 use InvalidArgumentException
;
9 use MediaWiki\MediaWikiServices
;
10 use MediaWiki\Revision\RevisionAccessException
;
11 use MediaWiki\Revision\RevisionStore
;
12 use MediaWiki\Revision\SlotRoleRegistry
;
13 use MediaWiki\Revision\SlotRecord
;
14 use MediaWiki\Storage\SqlBlobStore
;
15 use MediaWikiTestCase
;
19 use Wikimedia\Rdbms\Database
;
20 use Wikimedia\Rdbms\LoadBalancer
;
21 use Wikimedia\TestingAccessWrapper
;
24 class RevisionStoreTest
extends MediaWikiTestCase
{
26 private function useTextId() {
27 global $wgMultiContentRevisionSchemaMigrationStage;
29 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD
);
33 * @param LoadBalancer $loadBalancer
34 * @param SqlBlobStore $blobStore
35 * @param WANObjectCache $WANObjectCache
37 * @return RevisionStore
39 private function getRevisionStore(
42 $WANObjectCache = null
44 global $wgMultiContentRevisionSchemaMigrationStage;
45 // the migration stage should be irrelevant, since all the tests that interact with
46 // the database are in RevisionStoreDbTest, not here.
48 return new RevisionStore(
49 $loadBalancer ?
: $this->getMockLoadBalancer(),
50 $blobStore ?
: $this->getMockSqlBlobStore(),
51 $WANObjectCache ?
: $this->getHashWANObjectCache(),
52 MediaWikiServices
::getInstance()->getCommentStore(),
53 MediaWikiServices
::getInstance()->getContentModelStore(),
54 MediaWikiServices
::getInstance()->getSlotRoleStore(),
55 MediaWikiServices
::getInstance()->getSlotRoleRegistry(),
56 $wgMultiContentRevisionSchemaMigrationStage,
57 MediaWikiServices
::getInstance()->getActorMigration()
62 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
64 private function getMockLoadBalancer() {
65 return $this->getMockBuilder( LoadBalancer
::class )
66 ->disableOriginalConstructor()->getMock();
70 * @return \PHPUnit_Framework_MockObject_MockObject|Database
72 private function getMockDatabase() {
73 return $this->getMockBuilder( Database
::class )
74 ->disableOriginalConstructor()->getMock();
78 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
80 private function getMockSqlBlobStore() {
81 return $this->getMockBuilder( SqlBlobStore
::class )
82 ->disableOriginalConstructor()->getMock();
86 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
88 private function getMockCommentStore() {
89 return $this->getMockBuilder( CommentStore
::class )
90 ->disableOriginalConstructor()->getMock();
94 * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
96 private function getMockSlotRoleRegistry() {
97 return $this->getMockBuilder( SlotRoleRegistry
::class )
98 ->disableOriginalConstructor()->getMock();
101 private function getHashWANObjectCache() {
102 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
105 public function provideSetContentHandlerUseDB() {
107 // ContentHandlerUseDB can be true of false pre migration.
108 [ false, SCHEMA_COMPAT_OLD
, false ],
109 [ true, SCHEMA_COMPAT_OLD
, false ],
110 // During and after migration it can not be false...
111 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
112 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
113 [ false, SCHEMA_COMPAT_NEW
, true ],
114 // ...but it can be true.
115 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
116 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
117 [ true, SCHEMA_COMPAT_NEW
, false ],
122 * @dataProvider provideSetContentHandlerUseDB
123 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
124 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
126 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
127 if ( $expectedFail ) {
128 $this->setExpectedException( MWException
::class );
131 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
133 $store = new RevisionStore(
134 $this->getMockLoadBalancer(),
135 $this->getMockSqlBlobStore(),
136 $this->getHashWANObjectCache(),
137 $this->getMockCommentStore(),
138 $nameTables->getContentModels(),
139 $nameTables->getSlotRoles(),
140 $this->getMockSlotRoleRegistry(),
142 MediaWikiServices
::getInstance()->getActorMigration()
145 $store->setContentHandlerUseDB( $contentHandlerDb );
146 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
149 public function testGetTitle_successFromPageId() {
150 $mockLoadBalancer = $this->getMockLoadBalancer();
151 // Title calls wfGetDB() so we have to set the main service
152 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
154 $db = $this->getMockDatabase();
155 // Title calls wfGetDB() which uses a regular Connection
156 $mockLoadBalancer->expects( $this->atLeastOnce() )
157 ->method( 'getConnection' )
160 // First call to Title::newFromID, faking no result (db lag?)
161 $db->expects( $this->at( 0 ) )
162 ->method( 'selectRow' )
168 ->willReturn( (object)[
169 'page_namespace' => '1',
170 'page_title' => 'Food',
173 $store = $this->getRevisionStore( $mockLoadBalancer );
174 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
176 $this->assertSame( 1, $title->getNamespace() );
177 $this->assertSame( 'Food', $title->getDBkey() );
180 public function testGetTitle_successFromPageIdOnFallback() {
181 $mockLoadBalancer = $this->getMockLoadBalancer();
182 // Title calls wfGetDB() so we have to set the main service
183 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
185 $db = $this->getMockDatabase();
186 // Title calls wfGetDB() which uses a regular Connection
187 // Assert that the first call uses a REPLICA and the second falls back to master
188 $mockLoadBalancer->expects( $this->exactly( 2 ) )
189 ->method( 'getConnection' )
191 // RevisionStore getTitle uses a ConnectionRef
192 $mockLoadBalancer->expects( $this->atLeastOnce() )
193 ->method( 'getConnectionRef' )
196 // First call to Title::newFromID, faking no result (db lag?)
197 $db->expects( $this->at( 0 ) )
198 ->method( 'selectRow' )
204 ->willReturn( false );
206 // First select using rev_id, faking no result (db lag?)
207 $db->expects( $this->at( 1 ) )
208 ->method( 'selectRow' )
210 [ 'revision', 'page' ],
214 ->willReturn( false );
216 // Second call to Title::newFromID, no result
217 $db->expects( $this->at( 2 ) )
218 ->method( 'selectRow' )
224 ->willReturn( (object)[
225 'page_namespace' => '2',
226 'page_title' => 'Foodey',
229 $store = $this->getRevisionStore( $mockLoadBalancer );
230 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
232 $this->assertSame( 2, $title->getNamespace() );
233 $this->assertSame( 'Foodey', $title->getDBkey() );
236 public function testGetTitle_successFromRevId() {
237 $mockLoadBalancer = $this->getMockLoadBalancer();
238 // Title calls wfGetDB() so we have to set the main service
239 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
241 $db = $this->getMockDatabase();
242 // Title calls wfGetDB() which uses a regular Connection
243 $mockLoadBalancer->expects( $this->atLeastOnce() )
244 ->method( 'getConnection' )
246 // RevisionStore getTitle uses a ConnectionRef
247 $mockLoadBalancer->expects( $this->atLeastOnce() )
248 ->method( 'getConnectionRef' )
251 // First call to Title::newFromID, faking no result (db lag?)
252 $db->expects( $this->at( 0 ) )
253 ->method( 'selectRow' )
259 ->willReturn( false );
261 // First select using rev_id, faking no result (db lag?)
262 $db->expects( $this->at( 1 ) )
263 ->method( 'selectRow' )
265 [ 'revision', 'page' ],
269 ->willReturn( (object)[
270 'page_namespace' => '1',
271 'page_title' => 'Food2',
274 $store = $this->getRevisionStore( $mockLoadBalancer );
275 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
277 $this->assertSame( 1, $title->getNamespace() );
278 $this->assertSame( 'Food2', $title->getDBkey() );
281 public function testGetTitle_successFromRevIdOnFallback() {
282 $mockLoadBalancer = $this->getMockLoadBalancer();
283 // Title calls wfGetDB() so we have to set the main service
284 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
286 $db = $this->getMockDatabase();
287 // Title calls wfGetDB() which uses a regular Connection
288 // Assert that the first call uses a REPLICA and the second falls back to master
289 $mockLoadBalancer->expects( $this->exactly( 2 ) )
290 ->method( 'getConnection' )
292 // RevisionStore getTitle uses a ConnectionRef
293 $mockLoadBalancer->expects( $this->atLeastOnce() )
294 ->method( 'getConnectionRef' )
297 // First call to Title::newFromID, faking no result (db lag?)
298 $db->expects( $this->at( 0 ) )
299 ->method( 'selectRow' )
305 ->willReturn( false );
307 // First select using rev_id, faking no result (db lag?)
308 $db->expects( $this->at( 1 ) )
309 ->method( 'selectRow' )
311 [ 'revision', 'page' ],
315 ->willReturn( false );
317 // Second call to Title::newFromID, no result
318 $db->expects( $this->at( 2 ) )
319 ->method( 'selectRow' )
325 ->willReturn( false );
327 // Second select using rev_id, result
328 $db->expects( $this->at( 3 ) )
329 ->method( 'selectRow' )
331 [ 'revision', 'page' ],
335 ->willReturn( (object)[
336 'page_namespace' => '2',
337 'page_title' => 'Foodey',
340 $store = $this->getRevisionStore( $mockLoadBalancer );
341 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
343 $this->assertSame( 2, $title->getNamespace() );
344 $this->assertSame( 'Foodey', $title->getDBkey() );
348 * @covers \MediaWiki\Revision\RevisionStore::getTitle
350 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
351 $mockLoadBalancer = $this->getMockLoadBalancer();
352 // Title calls wfGetDB() so we have to set the main service
353 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
355 $db = $this->getMockDatabase();
356 // Title calls wfGetDB() which uses a regular Connection
357 // Assert that the first call uses a REPLICA and the second falls back to master
359 // RevisionStore getTitle uses getConnectionRef
360 // Title::newFromID uses getConnection
361 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
362 $mockLoadBalancer->expects( $this->exactly( 2 ) )
364 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
365 static $callCounter = 0;
367 // The first call should be to a REPLICA, and the second a MASTER.
368 if ( $callCounter === 1 ) {
369 $this->assertSame( DB_REPLICA
, $masterOrReplica );
370 } elseif ( $callCounter === 2 ) {
371 $this->assertSame( DB_MASTER
, $masterOrReplica );
376 // First and third call to Title::newFromID, faking no result
377 foreach ( [ 0, 2 ] as $counter ) {
378 $db->expects( $this->at( $counter ) )
379 ->method( 'selectRow' )
385 ->willReturn( false );
388 foreach ( [ 1, 3 ] as $counter ) {
389 $db->expects( $this->at( $counter ) )
390 ->method( 'selectRow' )
392 [ 'revision', 'page' ],
396 ->willReturn( false );
399 $store = $this->getRevisionStore( $mockLoadBalancer );
401 $this->setExpectedException( RevisionAccessException
::class );
402 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
405 public function provideNewRevisionFromRow_legacyEncoding_applied() {
406 yield
'windows-1252, old_flags is empty' => [
411 'old_text' => "S\xF6me Content",
416 yield
'windows-1252, old_flags is null' => [
421 'old_text' => "S\xF6me Content",
428 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
430 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
432 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
433 if ( !$this->useTextId() ) {
434 $this->markTestSkipped( 'No longer applicable with MCR schema' );
437 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
438 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
440 $blobStore = new SqlBlobStore( $lb, $cache );
441 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
443 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
445 $record = $store->newRevisionFromRow(
446 $this->makeRow( $row ),
448 Title
::newFromText( __METHOD__
. '-UTPage' )
451 $this->assertSame( $text, $record->getContent( SlotRecord
::MAIN
)->serialize() );
455 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
457 public function testNewRevisionFromRow_legacyEncoding_ignored() {
458 if ( !$this->useTextId() ) {
459 $this->markTestSkipped( 'No longer applicable with MCR schema' );
463 'old_flags' => 'utf-8',
464 'old_text' => 'Söme Content',
467 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
468 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
470 $blobStore = new SqlBlobStore( $lb, $cache );
471 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
473 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
475 $record = $store->newRevisionFromRow(
476 $this->makeRow( $row ),
478 Title
::newFromText( __METHOD__
. '-UTPage' )
480 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord
::MAIN
)->serialize() );
483 private function makeRow( array $array ) {
487 'rev_timestamp' => '20110101000000',
488 'rev_user_text' => 'Tester',
490 'rev_minor_edit' => 0,
493 'rev_parent_id' => 0,
494 'rev_sha1' => 'deadbeef',
495 'rev_comment_text' => 'Testing',
496 'rev_comment_data' => '{}',
497 'rev_comment_cid' => 111,
498 'page_namespace' => 0,
499 'page_title' => 'TEST',
502 'page_is_redirect' => 0,
504 'user_name' => 'Tester',
507 if ( $this->useTextId() ) {
509 'rev_content_format' => CONTENT_FORMAT_TEXT
,
510 'rev_content_model' => CONTENT_MODEL_TEXT
,
513 'old_text' => 'Hello World',
514 'old_flags' => 'utf-8',
517 if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
519 'main' => new WikitextContent( $array['old_text'] ),
527 public function provideMigrationConstruction() {
529 [ SCHEMA_COMPAT_OLD
, false ],
530 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
531 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
532 [ SCHEMA_COMPAT_NEW
, false ],
533 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
534 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
535 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
540 * @covers \MediaWiki\Revision\RevisionStore::__construct
541 * @dataProvider provideMigrationConstruction
543 public function testMigrationConstruction( $migration, $expectException ) {
544 if ( $expectException ) {
545 $this->setExpectedException( InvalidArgumentException
::class );
547 $loadBalancer = $this->getMockLoadBalancer();
548 $blobStore = $this->getMockSqlBlobStore();
549 $cache = $this->getHashWANObjectCache();
550 $commentStore = $this->getMockCommentStore();
551 $services = MediaWikiServices
::getInstance();
552 $nameTables = $services->getNameTableStoreFactory();
553 $contentModelStore = $nameTables->getContentModels();
554 $slotRoleStore = $nameTables->getSlotRoles();
555 $slotRoleRegistry = $services->getSlotRoleRegistry();
556 $store = new RevisionStore(
561 $nameTables->getContentModels(),
562 $nameTables->getSlotRoles(),
565 $services->getActorMigration()
567 if ( !$expectException ) {
568 $store = TestingAccessWrapper
::newFromObject( $store );
569 $this->assertSame( $loadBalancer, $store->loadBalancer
);
570 $this->assertSame( $blobStore, $store->blobStore
);
571 $this->assertSame( $cache, $store->cache
);
572 $this->assertSame( $commentStore, $store->commentStore
);
573 $this->assertSame( $contentModelStore, $store->contentModelStore
);
574 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
575 $this->assertSame( $migration, $store->mcrMigrationStage
);